package com.requestfactory.wire;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.NullCipher;

import com.requestfactory.wire.spi.WireSPIServerId;
import com.requestfactory.wire.spi.WireSPIStub;
import com.requestfactory.wire.spi.http.WireSPISkeletonSunHTTPServerImpl;
import com.requestfactory.wire.spi.http.WireSPIStubApacheHTTPClientImpl;

/**
 * Wire Framework single entry point.</p>
 * Use <code>getSkeleton</code> method to bind service 
 * interface and implementation and export it to network over HTTP.</p>
 * Use <code>createStub</code> method to create remote service proxy
 * object and invoke service over HTTP. 
 * @author Nikolay Barkhatov
 */
public enum Wire {
	
	INSTANCE;
	
	private final Map<WireSPIServerId,WireSkeleton> spiSkeletonMap =
		new HashMap<WireSPIServerId,WireSkeleton>();
	
	/**
	 * Get existing or create new Skeleton Server instance on given
	 * port and context.
	 * @param port HTTP Server port number.
	 * @param context HTTP Server context.
	 * @return <code>WireSkelelon</code> instance.
	 * @throws WireSkeletonException if Skeleton Server instance
	 * cannot be created.
	 */
	public synchronized WireSkeleton getSkeleton(
			int port, String context)
	throws WireSkeletonException {
		return getSkeleton(port, context, NULL_CIPHER);
	}
	
	static final Cipher NULL_CIPHER = new NullCipher();
	
	public synchronized WireSkeleton getSkeleton(
			int port, String context, Cipher cipher)
	throws WireSkeletonException {
		if(port < 0) {
			throw new IllegalArgumentException(
					"Input argument 'port' cannot be negative number");
		}
		if(context == null) {
			throw new IllegalArgumentException(
				"Input argument 'context' cannot be null");
		}
		assert port > -1;
		assert context != null;
		WireSPIServerId wireSPIServerId = 
			new WireSPIServerId(port, context, cipher);
		WireSkeleton skeleton = null;
		synchronized(spiSkeletonMap) {
			skeleton = spiSkeletonMap.get(wireSPIServerId);
			if(skeleton == null) {
				skeleton = new WireSkeleton(
						new WireSPISkeletonSunHTTPServerImpl(wireSPIServerId));
				spiSkeletonMap.put(wireSPIServerId, skeleton);
			}
		}
		assert skeleton != null;
		return skeleton;
	}

	/**
	 * Create service proxy object.
	 * @param <T> Service type.
	 * @param serviceClass Service class.
	 * @param wireSkeletonURL HTTP Service URL.
	 * @return Service proxy (stub) object.
	 * @throws WireStubException if cannot create proxy object.
	 */
	public <T> T createStub(
			final Class<T> serviceClass, 
			final String wireSkeletonURL) 
	throws WireStubException {
		return createStub(serviceClass, wireSkeletonURL, NULL_CIPHER);
	}
	
	@SuppressWarnings("unchecked")
	public <T> T createStub(
			final Class<T> serviceClass, 
			final String wireSkeletonURL,
			final Cipher cipher) 
	throws WireStubException {
		if(serviceClass == null) {
			throw new IllegalArgumentException(
					"Input argument 'serviceClass' cannot be null");
		}
		if(wireSkeletonURL == null || wireSkeletonURL.isEmpty()) {
			throw new IllegalArgumentException(
					"Input argument 'wireSkeletonURL' cannot be null or empty string");
		}
		if(cipher == null) {
			throw new IllegalArgumentException(
					"Input argument 'cipher' cannot be null");
		}
		assert serviceClass != null;
		assert wireSkeletonURL != null;
		assert cipher != null;
		T httpProxyInstance = 
			(T)Proxy.newProxyInstance(
					serviceClass.getClassLoader(),
					new Class[] {serviceClass},
	                new InvocationHandler() {						
						private final WireSPIStub wireSPIStub = 
							new WireSPIStubApacheHTTPClientImpl(wireSkeletonURL);
						@Override
						public Object invoke(
								Object proxy, 
								Method method,
								Object[] args) 
						throws Throwable {
							Serializable[] arguments = 
								new Serializable[args != null ? args.length : 0];
							for(int i = 0; i < arguments.length; ++i) {
								arguments[i] = (Serializable)args[i];
							}
							return wireSPIStub.executeMethod(
									serviceClass.getName(),
									method.getName(), 
									method.getParameterTypes(),
									arguments,
									cipher);
						}
				
			});
		return httpProxyInstance;
	}

}
